﻿// Original idea from
// https://multifileedmx.codeplex.com/
// This version by jsrsoft.co.uk, June 2013
//
// Namespaces are a pain!
// http://www.ibm.com/developerworks/xml/library/x-abolns/index.html
//
//
namespace MultiFileEDMX
{
    using Microsoft.Data.Entity.Design.Extensibility;
    using System;
    using System.Collections.Generic;
    using System.Data.Entity.Design;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Xml.Linq;

    /// <summary>
    /// MultiFileEDMXTools supports the optional saving and reading of the fours sections 
    /// of an EDMX into separate files. This can be of use in version control systems.
    /// A designer surface property is used to control whether this feature is
    /// enabled or not.
    /// Original was a static class. This version is an instance class as I wanted to
    /// be sure that such would work.
    /// </summary>
    class MultiFileEDMXTools
    {
        private static bool _enabled = false;
        private static XNamespace _edmxns = null;
        private readonly NamespaceManager _namespaceManager = null;
        private readonly String _defaultEDMXContentsV3 = String.Empty;

        /// <summary>
        /// Filename extensions for the four model files
        /// </summary>
        private readonly static string StorageModelFileExtention = "ssdl";
        private readonly static string ConceptualModelFileExtension = "csdl";
        private readonly static string MappingModelFileExtension = "msl";
        private readonly static string DesignerModelFileExtension = "des";

        public MultiFileEDMXTools()
        {
            _namespaceManager = new NamespaceManager();

            using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MultiFileEDMXTools.DefaultEdmxV3.xml"))
            using (StreamReader reader = new StreamReader(stream))
            {
                _defaultEDMXContentsV3 = reader.ReadToEnd();
            }
        }

        /// <summary>
        /// Loads storage/conceptual/mapping model and merges them into a single edmx model
        /// </summary>
        /// <param name="edmxDoc"></param>
        /// <param name="path"></param>
        public void LoadAndMergeModels(ModelTransformExtensionContext context, string path)
        {
            //The version of the Entity Framework loaded in PC.
            //The EF Designer offers an upgrade if the version of the EDMX is lower.
            var EfVersion = context.EntityFrameworkVersion;

            //This is the incoming EDMX document
            var edmxDoc = context.CurrentDocument;
            var version = _namespaceManager.GetVersionFromEDMXDocument(edmxDoc);

            //Namespace Edmx is use for Conceptual, Storage, Msl and Designer sections. It is the schemas that use other types.
            _edmxns = _namespaceManager.GetEDMXNamespaceForVersion(version);

            //No point in continuing of different. Let EF Designer sort it out.
            if (EfVersion.Major != version.Major)
                return;

            //There is always a Conceptual section and so we check whether the property is present and whether the feature is turned on.
            var property1 = edmxDoc.Descendants(XName.Get("MultiFileEDMXToolsEnabled", "http://schemas.tempuri.com/MultiFileEDMXToolsDesignerExtension")).FirstOrDefault();
            if (property1 == null)
            {
                _enabled = false;
            }
            else
            {
                _enabled = Convert.ToBoolean(property1.Value);
            }

            // If the feature is off, we do not load the files.
            if (!_enabled)
                return;

            try
            {
                //Assuming the three files are present we continue processing,
                //otherwise we ignore silently, as any later save will create them.
                if (File.Exists(Path.ChangeExtension(path, ConceptualModelFileExtension))
                    && File.Exists(Path.ChangeExtension(path, StorageModelFileExtention))
                    && File.Exists(Path.ChangeExtension(path, MappingModelFileExtension))
                    && File.Exists(Path.ChangeExtension(path, DesignerModelFileExtension))
                   )
                {
                    // Load the four sections from their respective files.
                    XDocument csdlDoc = XDocument.Load(Path.ChangeExtension(path, ConceptualModelFileExtension));
                    XDocument ssdlDoc = XDocument.Load(Path.ChangeExtension(path, StorageModelFileExtention));
                    XDocument mslDoc = XDocument.Load(Path.ChangeExtension(path, MappingModelFileExtension));
                    XDocument desDoc = XDocument.Load(Path.ChangeExtension(path, DesignerModelFileExtension));

                    // Obtain conceptual/storage/mapping/designer models from individual xdocuments
                    XElement csdl = csdlDoc.Element(XName.Get("ConceptualModels", _edmxns.NamespaceName));
                    XElement ssdl = ssdlDoc.Element(XName.Get("StorageModels", _edmxns.NamespaceName));
                    XElement msl = mslDoc.Element(XName.Get("Mappings", _edmxns.NamespaceName));
                    XElement des = desDoc.Element(XName.Get("Designer", _edmxns.NamespaceName));

                    // Obtain reference to conceptual/storage/mapping models from the incoming edmx document supplied in the context.
                    XElement conceptualModelsNode = edmxDoc.Descendants(XName.Get("ConceptualModels", _edmxns.NamespaceName)).FirstOrDefault();
                    XElement storageModelsNode = edmxDoc.Descendants(XName.Get("StorageModels", _edmxns.NamespaceName)).FirstOrDefault();
                    XElement mappingsNode = edmxDoc.Descendants(XName.Get("Mappings", _edmxns.NamespaceName)).FirstOrDefault();
                    XElement designerNode = edmxDoc.Descendants(XName.Get("Designer", _edmxns.NamespaceName)).FirstOrDefault();

                    // Replace the four models in the incoming EDMX with those we've loaded.
                    conceptualModelsNode.ReplaceWith(csdl);
                    storageModelsNode.ReplaceWith(ssdl);
                    mappingsNode.ReplaceWith(msl);
                    designerNode.ReplaceWith(des);

                    //Return to the Designer with the EDMX reconstructed.
                }
            }
            catch (Exception ex)
            {
                var e = new ExtensionError(String.Format("MultiFileEDMXTools - Load And Merge Models Error: ", ex.Message), ex.HResult, ExtensionErrorSeverity.Message);
                context.Errors.Add(e);
            }
        }

        /// <summary>
        /// Stores the four sections from single edmx model into separate files.
        /// </summary>
        /// <param name="edmxDoc"></param>
        /// <param name="path"></param>
        public void StoreSeparateModels(ModelTransformExtensionContext context, string path)
        {
            try
            {
                //The Edmx document always exists and [hopefully] contains the designer surface property that
                //we use to determine whether MultiFileEDMXTools is enabled.
                //The User may have set to true in this session to be saved.
                var edmxDoc = context.CurrentDocument;

                //Check for existance and enabled. The property is stored in the Conceptual section.
                var property = edmxDoc.Descendants(XName.Get("MultiFileEDMXToolsEnabled", "http://schemas.tempuri.com/MultiFileEDMXToolsDesignerExtension")).FirstOrDefault();
                if (property == null)
                {
                    //If not present, we default to false;
                    _enabled = false;
                }
                else
                {
                    //If present and false we accept incoming EDMX..
                    _enabled = Convert.ToBoolean(property.Value);
                }

                if (!_enabled)
                    return;

                //Prepare the filenames for saving the four sections of the EDMX.
                //This will have the same path as the EMDX.
                string conceptualModelFileName = Path.ChangeExtension(path, ConceptualModelFileExtension);
                string storageModelFileName = Path.ChangeExtension(path, StorageModelFileExtention);
                string mappingsModelFileName = Path.ChangeExtension(path, MappingModelFileExtension);
                string designerModelFileName = Path.ChangeExtension(path, DesignerModelFileExtension);

                //Get references to the four sections in the incoming EMDX.
                XElement conceptualModelsNode = edmxDoc.Descendants(XName.Get("ConceptualModels", _edmxns.NamespaceName)).FirstOrDefault();
                XElement storageModelsNode = edmxDoc.Descendants(XName.Get("StorageModels", _edmxns.NamespaceName)).FirstOrDefault();
                XElement mappingsNode = edmxDoc.Descendants(XName.Get("Mappings", _edmxns.NamespaceName)).FirstOrDefault();
                XElement designerNode = edmxDoc.Descendants(XName.Get("Designer", _edmxns.NamespaceName)).FirstOrDefault();

                //Save the conceptual, storage, mapping and designer sections from edmx document in separate documents
                conceptualModelsNode.Save(conceptualModelFileName, SaveOptions.None);
                storageModelsNode.Save(storageModelFileName, SaveOptions.None);
                mappingsNode.Save(mappingsModelFileName, SaveOptions.None);
                designerNode.Save(designerModelFileName, SaveOptions.None);

                //Replace the current EDMX document with the default from the embedded resource.
                //This is the documemnt that is saved to the EDMX, but changing to it here does not affect
                //the document on display in the designer. Using an EmbededResource is a very
                //convenient way of making available an xml document.
                edmxDoc = XDocument.Parse(_defaultEDMXContentsV3, LoadOptions.None);

                //Re-request references to the 4 sections from the now default EDMX.
                conceptualModelsNode = edmxDoc.Descendants(XName.Get("ConceptualModels", _edmxns.NamespaceName)).FirstOrDefault();
                storageModelsNode = edmxDoc.Descendants(XName.Get("StorageModels", _edmxns.NamespaceName)).FirstOrDefault();
                mappingsNode = edmxDoc.Descendants(XName.Get("Mappings", _edmxns.NamespaceName)).FirstOrDefault();
                designerNode = edmxDoc.Descendants(XName.Get("Designer", _edmxns.NamespaceName)).FirstOrDefault();

                //Add the stub comment, which needs to be done at runtime due to the filename.
                //We assume the Stub will never be already present in the default EDMX.
                conceptualModelsNode.AddFirst(new XComment("Stub for conceptual model; model is defined in " + Path.GetFileName(conceptualModelFileName)));
                storageModelsNode.AddFirst(new XComment("Stub for storage model; model is defined in " + Path.GetFileName(storageModelFileName)));
                mappingsNode.AddFirst(new XComment("Stub for mappings model; mappings are defined in " + Path.GetFileName(mappingsModelFileName)));
                designerNode.AddFirst(new XComment("Stub for diagrams model; diagrams are defined in " + Path.GetFileName(designerModelFileName)));

                //Ensure the MultiFileEDMXToolsEnabled property is present and updated in the default EDMX, since this will be
                //the first file read into the designer for the User's next editing session.
                //This is the quickest way of updating the value of of MultiFileEDMXToolsEnabled, while checking whether it's present.
                //First we remove it if present, then add this local copy.
                var ret = conceptualModelsNode.Descendants(XName.Get("MultiFileEDMXToolsEnabled", @"http://schemas.tempuri.com/MultiFileEDMXToolsDesignerExtension")).FirstOrDefault();
                if (ret != null)
                    ret.Remove();
                conceptualModelsNode.Add(property);

                context.CurrentDocument = edmxDoc;
            }
            catch (Exception ex)
            {
                var e = new ExtensionError(ex.Message, ex.HResult, ExtensionErrorSeverity.Message);
                context.Errors.Add(e);
            }
        }
    }

    /// <summary>
    /// Provides Namespace for each of the EDMX versions.
    /// From EdmGen2, although the codeplex source of EF itself
    /// has a very simmilar class.
    /// </summary>
    public class NamespaceManager
    {
        private static Version v1 = EntityFrameworkVersions.Version1;
        private static Version v2 = EntityFrameworkVersions.Version2;
        private static Version v3 = EntityFrameworkVersions.Version3;

        private Dictionary<Version, XNamespace> _versionToCSDLNamespace = new Dictionary<Version, XNamespace>() 
        { 
        { v1, XNamespace.Get("http://schemas.microsoft.com/ado/2006/04/edm") }, 
        { v2, XNamespace.Get("http://schemas.microsoft.com/ado/2008/09/edm") },
        { v3, XNamespace.Get("http://schemas.microsoft.com/ado/2009/11/edm") }
        };

        private Dictionary<Version, XNamespace> _versionToSSDLNamespace = new Dictionary<Version, XNamespace>() 
        { 
        { v1, XNamespace.Get("http://schemas.microsoft.com/ado/2006/04/edm/ssdl") }, 
        { v2, XNamespace.Get("http://schemas.microsoft.com/ado/2009/02/edm/ssdl") },
        { v3, XNamespace.Get("http://schemas.microsoft.com/ado/2009/11/edm/ssdl") } 
        };

        private Dictionary<Version, XNamespace> _versionToMSLNamespace = new Dictionary<Version, XNamespace>() 
        { 
        { v1, XNamespace.Get("urn:schemas-microsoft-com:windows:storage:mapping:CS") }, 
        { v2, XNamespace.Get("http://schemas.microsoft.com/ado/2008/09/mapping/cs") },
        { v3, XNamespace.Get("http://schemas.microsoft.com/ado/2009/11/mapping/cs") }
        };

        private Dictionary<Version, XNamespace> _versionToEDMXNamespace = new Dictionary<Version, XNamespace>() 
        { 
        { v1, XNamespace.Get("http://schemas.microsoft.com/ado/2007/06/edmx") }, 
        { v2, XNamespace.Get("http://schemas.microsoft.com/ado/2008/10/edmx") },
        { v3, XNamespace.Get("http://schemas.microsoft.com/ado/2009/11/edmx") }
        };

        private Dictionary<XNamespace, Version> _namespaceToVersion = new Dictionary<XNamespace, Version>();

        internal NamespaceManager()
        {
            foreach (KeyValuePair<Version, XNamespace> kvp in _versionToCSDLNamespace)
            {
                _namespaceToVersion.Add(kvp.Value, kvp.Key);
            }

            foreach (KeyValuePair<Version, XNamespace> kvp in _versionToSSDLNamespace)
            {
                _namespaceToVersion.Add(kvp.Value, kvp.Key);
            }

            foreach (KeyValuePair<Version, XNamespace> kvp in _versionToMSLNamespace)
            {
                _namespaceToVersion.Add(kvp.Value, kvp.Key);
            }

            foreach (KeyValuePair<Version, XNamespace> kvp in _versionToEDMXNamespace)
            {
                _namespaceToVersion.Add(kvp.Value, kvp.Key);
            }
        }

        internal Version GetVersionFromEDMXDocument(XDocument xdoc)
        {
            XElement el = xdoc.Root;
            if (el.Name.LocalName.Equals("Edmx") == false)
            {
                throw new ArgumentException("Unexpected root node local name for edmx document");
            }
            return this.GetVersionForNamespace(el.Name.Namespace);
        }

        internal Version GetVersionFromCSDLDocument(XDocument xdoc)
        {
            XElement el = xdoc.Root;
            if (el.Name.LocalName.Equals("Schema") == false)
            {
                throw new ArgumentException("Unexpected root node local name for csdl document");
            }
            return this.GetVersionForNamespace(el.Name.Namespace);
        }

        internal XNamespace GetMSLNamespaceForVersion(Version v)
        {
            XNamespace n;
            _versionToMSLNamespace.TryGetValue(v, out n);
            return n;
        }

        internal XNamespace GetCSDLNamespaceForVersion(Version v)
        {
            XNamespace n;
            _versionToCSDLNamespace.TryGetValue(v, out n);
            return n;
        }

        internal XNamespace GetSSDLNamespaceForVersion(Version v)
        {
            XNamespace n;
            _versionToSSDLNamespace.TryGetValue(v, out n);
            return n;
        }

        internal XNamespace GetEDMXNamespaceForVersion(Version v)
        {
            XNamespace n;
            _versionToEDMXNamespace.TryGetValue(v, out n);
            return n;
        }

        //Designer namespace is same as EDMX
        internal XNamespace GetDESNamespaceForVersion(Version v)
        {
            XNamespace n;
            _versionToEDMXNamespace.TryGetValue(v, out n);
            return n;
        }

        internal Version GetVersionForNamespace(XNamespace n)
        {
            Version v;
            _namespaceToVersion.TryGetValue(n, out v);
            return v;
        }
    }
}
